package com.valsong.sandbox.mock.invoker;

import com.alibaba.jvm.sandbox.api.listener.ext.Advice;
import com.valsong.sandbox.mock.Invoker;
import com.valsong.sandbox.mock.classloader.ComposeClassLoader;
import com.valsong.sandbox.mock.exception.MockFailedException;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyRuntimeException;
import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.AccessibleObject;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;


/**
 * AbstractGroovyInvoker
 *
 * @author Val Song
 */
@Slf4j
public abstract class AbstractGroovyInvoker implements Invoker {

    private ClassLoader moduleJarClassLoader = AbstractGroovyInvoker.class.getClassLoader();

    private ConcurrentMap<ClassLoader, GroovyClassLoader> groovyClassLoaderRepository = new ConcurrentHashMap<>();

    private ConcurrentMap<String, GroovyObject> groovyInstanceRepository = new ConcurrentHashMap<>();

    @Override
    public Object invoke(String script, String mockMethod, Advice advice) {


        GroovyObject instance = groovyInstance(script, advice);
        try {
            Object[] arguments = advice.getParameterArray();

            log.debug("groovyClassLoaderRepository:[{}] groovyInstanceRepository:[{}]", groovyClassLoaderRepository, groovyInstanceRepository);

            log.debug("Prepare to invoke groovy method! script:[{}] mockMethod:[{}] arguments:[{}] ", script, mockMethod, arguments);

            Object mockResult = instance.invokeMethod(mockMethod, arguments);

            return mockResult;

        } catch (GroovyRuntimeException e) {
            AccessibleObject method = advice.getBehavior().getTarget();
            String errorMsg = String.format("Can't invoke groovy method, please check! script:[%s] mockMethod:[%s] method:[%s]", script, mockMethod, method);
            throw new MockFailedException(errorMsg, e);
        } catch (Throwable e) {
            throw e;
        }

    }

    private GroovyObject groovyInstance(String script, Advice advice) {
        return groovyInstanceRepository.computeIfAbsent(script, (groovyScript) -> {

            ClassLoader contextClassLoader = advice.getLoader();

            GroovyClassLoader groovyClassLoader = groovyClassLoaderRepository.computeIfAbsent(contextClassLoader, (loader) -> {

                // 组合moduleJarClassLoader和contextClassLoader
                // 使用moduleJarClassLoader来加载groovy相关的class
                // 使用contextClassLoader来加载业务相关的class
                ClassLoader composeClassLoader = new ComposeClassLoader(moduleJarClassLoader, contextClassLoader);

                return new GroovyClassLoader(composeClassLoader);
            });

            log.debug("Prepared to create new Groovy instance! script:[{}] moduleJarClassLoader:[{}] moduleJarClassLoaderParent:[{}] " +
                            "contextClassLoader:[{}] contextClassLoaderParent:[{}] groovyClassLoader:[{}]",
                    groovyScript,
                    moduleJarClassLoader,
                    Optional.ofNullable(moduleJarClassLoader).map(ClassLoader::getParent).orElse(null),
                    contextClassLoader,
                    Optional.ofNullable(contextClassLoader).map(ClassLoader::getParent).orElse(null),
                    groovyClassLoader);
            try {
                GroovyCodeSource groovyCodeSource = getGroovyCodeSource(groovyScript);

                Class<?> groovyClazz = groovyClassLoader.parseClass(groovyCodeSource);

                //new Instance
                return (GroovyObject) groovyClazz.getDeclaredConstructor().newInstance();
            } catch (Throwable e) {
                String errorMsg = String.format("Create groovy instance failed! script:[%s] moduleJarClassLoader:[%s] moduleJarClassLoaderParent:[%s]" +
                                " contextClassLoader:[%s] contextClassLoaderParent:[%s] groovyClassLoader:[%s]",
                        groovyScript,
                        moduleJarClassLoader,
                        Optional.ofNullable(moduleJarClassLoader).map(ClassLoader::getParent).orElse(null),
                        contextClassLoader,
                        Optional.ofNullable(contextClassLoader).map(ClassLoader::getParent).orElse(null),
                        groovyClassLoader);
                throw new MockFailedException(errorMsg, e);
            }
        });
    }

    /**
     * 获取GroovyCodeSource
     *
     * @param script groovy文件路径或者脚本
     * @return
     */
    protected abstract GroovyCodeSource getGroovyCodeSource(String script);
}
